package com.xceptance.xlt.common.util.action.validation; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.json.JSONObject; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import com.gargoylesoftware.htmlunit.WebResponse; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.xceptance.xlt.api.util.XltLogger; import com.xceptance.xlt.common.util.ConcreteNodeList; import com.xceptance.xlt.common.util.ParameterUtils; /** * <p> * Implementation of {@link XPathGetable}. <br> * The class is intended to offer ways to select elements by passing a xpath for {@link WebReponse} objects, whose body * can NOT be parsed into a {@link HtmlPage}. E.g, XML or JSON content. <br> * </p> * <p> * In this case it is tried to offer alternative parsing possibilities to allow the selection of elements by xpath. If * this is also not possible and it is tried to select elements by xpah, it throws an IllegalArgumentException. * </p> * <ul> * <li>Parses WebResponse only if the mime-type is supported (see {@link #SUPPORTEDHEADERCONTENTTYPES supported types}). * <li>Use {@link #isXPathable(mime-type)} to see if a response is parsable. * <li>Parses automatically and only when needed via {@link #getByXPath(String)}. * <li>If the content of a WebResponse is not parsable, but the method {@link #getByXPath(String)} is not calles, * nothing happens. * </ul> * * @author matthias mitterreiter */ public class XPathWithNonParseableWebResponse implements XPathGetable { private WebResponse webResponse; private XPath xPath; private Document xmlInputSource; static final HashMap<String, String> SUPPORTEDHEADERCONTENTTYPES = new HashMap<String, String>(); static final String JSON = "json"; static final String XML = "xml"; static { SUPPORTEDHEADERCONTENTTYPES.put("application/json", JSON); SUPPORTEDHEADERCONTENTTYPES.put("text/json", JSON); SUPPORTEDHEADERCONTENTTYPES.put("text/x-json", JSON); SUPPORTEDHEADERCONTENTTYPES.put("text/xml", XML); SUPPORTEDHEADERCONTENTTYPES.put("application/xml", XML); } public XPathWithNonParseableWebResponse(final WebResponse webResponse) { XltLogger.runTimeLogger.debug("Creating new Instance"); setWebResponse(webResponse); } private void setWebResponse(final WebResponse webResponse) { ParameterUtils.isNotNull(webResponse, "WebResponse"); final String contentType = webResponse.getContentType(); if (isXPathable(contentType)) { this.webResponse = webResponse; } else { throw new IllegalArgumentException("Parsing content of type: '" + contentType + "' is not supported"); } } /** * Parses the {@link WebResponse} in a format that allows to select elements by xpath, but only if necessary and * only once. <br> * The elements then get parsed to Strings <br> * * @throws IllegalArgumentException * if it is not possible to parse and select. */ @Override public List<String> getByXPath(final String xPath) { List<String> resultList = new ArrayList<String>(); try { resultList = getByXPathFromInputSource(xPath); } catch (final Exception e) { throw new IllegalArgumentException("Failed to fetch Elements: " + e.getMessage(), e); } return resultList; } private List<String> getByXPathFromInputSource(final String xPath) throws XPathExpressionException, ParserConfigurationException, SAXException, IOException { loadContentFromWebResponseIfNecessary(); final NodeList nodeList = createNodeListByXPathFromInputSource(xPath); final List<String> resultList = createStringListFromNodeList(nodeList); return resultList; } private void loadContentFromWebResponseIfNecessary() throws ParserConfigurationException, SAXException, IOException { loadXMLSourceFromWebResponseIfNecessary(); createXPathIfNecessary(); } private void loadXMLSourceFromWebResponseIfNecessary() throws ParserConfigurationException, SAXException, IOException { if (this.xmlInputSource == null) { createXMLSourceFromWebResponseContent(); } } private void createXPathIfNecessary() { if (this.xPath == null) { createXPath(); } } private List<String> createStringListFromNodeList(final NodeList nodeList) { final List<String> resultList = new ArrayList<String>(); for (int i = 0; i < nodeList.getLength(); i++) { final Node n = nodeList.item(i); resultList.add(n.getTextContent()); } return resultList; } private NodeList createNodeListByXPathFromInputSource(final String xPath) { XltLogger.runTimeLogger.debug("Getting Elements by XPath: " + xPath); NodeList list = new ConcreteNodeList(); try { list = (NodeList) this.xPath.compile(xPath).evaluate(this.xmlInputSource, XPathConstants.NODESET); } catch (final Exception e) { XltLogger.runTimeLogger.debug("Failed to get Elements: " + e.getMessage()); } return list; } private void createXPath() { XltLogger.runTimeLogger.debug("Creating new XPath"); final XPathFactory xpathFactory = XPathFactory.newInstance(); this.xPath = xpathFactory.newXPath(); } private void createXMLSourceFromWebResponseContent() throws ParserConfigurationException, SAXException, IOException { XltLogger.runTimeLogger.debug("Loading content from WebResponse"); final String contentType = getContentTypeFromWebResponse(); final String bodyContent = this.webResponse.getContentAsString(); if (JSON.equals(contentType)) { this.xmlInputSource = createXMLSourceFromJson(bodyContent); } else if (XML.equals(contentType)) { this.xmlInputSource = createXMLSourceFromXML(bodyContent); } else { throw new IllegalArgumentException("This will never happen!"); } } private String getContentTypeFromWebResponse() { final String contentType = this.webResponse.getContentType(); final String resolvedContentType = SUPPORTEDHEADERCONTENTTYPES.get(contentType); return resolvedContentType; } private Document createXMLSourceFromJson(final String json) throws ParserConfigurationException, SAXException, IOException { XltLogger.runTimeLogger.debug("Converting Json Content to XML"); String xmlString; xmlString = org.json.XML.toString(new JSONObject(json)); xmlString = "<json>" + xmlString + "</json>"; final Document document = createDocumentFromXmlString(xmlString); return document; } private Document createDocumentFromXmlString(final String xmlString) throws SAXException, IOException, ParserConfigurationException { final InputSource source = new InputSource(new StringReader(xmlString)); final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); final DocumentBuilder db = dbf.newDocumentBuilder(); final Document document = db.parse(source); return document; } private Document createXMLSourceFromXML(final String xmlString) throws SAXException, IOException, ParserConfigurationException { XltLogger.runTimeLogger.debug("Loading XML Content"); final Document document = createDocumentFromXmlString(xmlString); return document; } /** * @param contentType * mime-type of the WebResponse * @return true only if the content can be parsed so that elements can be selected by xpath. */ public boolean isXPathable(final String contentType) { return SUPPORTEDHEADERCONTENTTYPES.containsKey(contentType); } }